EVCacheValue: opt-in compact binary serialization with backwards-compatible reads#196
Merged
Merged
Conversation
d443e30 to
3892873
Compare
d46bf97 to
3444f24
Compare
bihaoxwork
reviewed
Jun 12, 2026
75d8249 to
40446ae
Compare
bihaoxwork
approved these changes
Jun 23, 2026
shy-1234
reviewed
Jun 29, 2026
shy-1234
reviewed
Jun 29, 2026
shy-1234
approved these changes
Jun 29, 2026
b7fdc7f to
40446ae
Compare
2 tasks
14c1cd2 to
59203c0
Compare
…atible reads
Hashed-key values are wrapped in an EVCacheValue envelope
(key, value, flags, ttl, createTime) currently serialized with Java
ObjectOutputStream, adding ~50-80 bytes of structural overhead per item.
This adds a compact, length-prefixed binary format for the envelope while
remaining fully backwards-compatible on reads, plus a first-class typed
property bundle (EVCacheTranscoderProperties) that lives on the base
transcoder and drives the transcoder's runtime behavior.
## Wire format
[magic 0x0C][reserved 0x00][int keyLen][key UTF-8][int valLen][value]
[int flags][long ttl][long createTime]
- Leading magic byte disambiguates from Java's 0xAC 0xED.
- Reserved byte at index 1 is read-and-ignored today; future readers
can branch on it for backwards-compatible format changes.
- Reads auto-detect format by leading byte: 0xAC 0xED -> Java, 0x0C -> binary.
## New classes
- EVCacheValueSerde (com.netflix.evcache.pool): the codec. Bounds-checks
length prefixes before allocating; corrupt payloads warn-log the failing
field + truncated hex dump (via Apache Commons Hex.encodeHexString, capped
at 1024 bytes) and return null. Matches BaseSerializingTranscoder's
resilience contract: corruption -> cache miss -> caller refills.
- EVCacheTranscoderProperties (com.netflix.evcache.config): typed access
to the FastProperties that govern transcoder behavior. Three keys today
(BINARY_SERIALIZATION_ENABLED, MAX_DATA_SIZE_BYTES, COMPRESSION_THRESHOLD_BYTES).
The Key enum is the extension point for future keys. Per-app -> global ->
static-default resolution chain; static value cached at construction for
hot-path reads.
## EVCacheSerializingTranscoder integration
The bundle is a protected final field on the base class:
protected final EVCacheTranscoderProperties properties;
Subclasses inherit one source of truth for FP resolution. Legacy
constructors are preserved and internally build a null-appName bundle.
## Feature Property rollout gate
Resolved through EVCacheTranscoderProperties with the three-level fallback:
- Per-app: <appName>.binary.serialization.enabled
- Global: default.evcache.binary.serialization.enabled
- Default: false
Read once at client construction and cached as a primitive on the
(immutable) transcoder ==> deploy/restart required to take effect; NOT a
live runtime toggle. Flip the property, then redeploy the consuming app.
## Compatibility
- New clients decode existing Java-serialized values unchanged (dual-format read).
- With the FP off (default), wire output is byte-identical to today.
- Corrupt binary payloads degrade to cache miss (null), matching the Java path.
- Public EVCacheTranscoder constructors (0-arg, int, int int) preserved.
## Testing
- EVCacheValueSerdeTest — 17 cases via public transcoder API: binary
round-trip edge cases, wire-shape assertions, backwards-compat with
legacy Java bytes, non-EVCacheValue passthrough, size-win, malformed
binary paths, forward-compat with optional additive fields.
- EVCacheTranscoderPropertiesTest — 15 cases (5 per key) covering the
three-level resolution chain. Property keys are asserted as string
literals so an enum rename fails the tests loudly.
- ./gradlew :evcache-core:test green.
- Smoke-tested end-to-end against a locally-published snapshot in a Spring
Boot Netflix sample app; round-trip put/get/delete on hashed keys passes.
Chunked-payload integration remains covered by EVCacheClient's existing
byte-level assembleChunks + CRC path; the binary format introduces no new
chunking risk by construction.
59203c0 to
c38f913
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Hashed-key values are wrapped in an
EVCacheValueenvelope(key, value, flags, ttl, createTime)that is currently serialized with JavaObjectOutputStream, adding ~50–80 bytes of structural overhead per item. This adds a compact, length-prefixed binary format for the envelope while remaining fully backwards-compatible on reads.This PR is two commits intended to be squashed at merge:
EVCacheValue: opt-in compact binary serde with backwards-compatible read— the wire format, transcoder dispatch, and the FP-gated rollout switch.Route the EVCacheValue binary-serialization toggle through EVCacheTranscoderProperties— extracts the FP resolution into a typed bundle with a per-app → global → static-default chain.What changed
EVCacheValueSerdeclass (com.netflix.evcache.pool) — public-final-non-instantiable codec, owns the wire format and all error handling:static byte[] serialize(EVCacheValue)— length-prefixed binary layout:[magic 0x0C][reserved 0x00][int keyLen][key UTF-8][int valLen][value][int flags][long ttl][long createTime].static EVCacheValue deserialize(byte[])— bounds-checks length prefixes before allocating; on any corruption / unexpected exception warn-logs the failing field and a truncated hex dump (via Apache CommonsHex.encodeHexString, capped at 1024 bytes) and returnsnull. MatchesBaseSerializingTranscoder's resilience contract (corruption → cache miss → caller refills from source of truth) so a single corrupt entry never crashes a get / getBulk / async pipeline.static boolean isBinaryFormat(byte[])— exposed for the dispatcher.EVCacheTranscoderbecomes a thin dispatcher (no try/catch):serialize: gates onproperties.isBinarySerializationEnabled() && o instanceof EVCacheValue→EVCacheValueSerde.serialize; elsesuper.serialize(Java).deserialize: dispatches onEVCacheValueSerde.isBinaryFormat→EVCacheValueSerde.deserialize; elsesuper.deserialize.EVCacheValuestays a pure POJO (codec moved out; constructor unchanged from pre-PR).EVCacheTranscoderProperties(new bundle incom.netflix.evcache.config) — typed access to the FPs that governEVCacheTranscoder. Today it holds one entry (USE_BINARY_SERIALIZATION); theKeyenum is the extension point for future transcoder properties. Resolution chain per property: per-app override → global default → static default, read once at construction and cached. AgetProperty(Key, Class, default)escape hatch supports future dynamic reads without forcing every caller off the cached primitive.EVCacheImplconstructs anEVCacheTranscoderPropertiesfor the envelope transcoder and injects it; max size is still read inline; compression stays pinned atInteger.MAX_VALUEso the leading magic byte is never gzip-rewritten.0xAC 0xED= legacy Java,0x0C= binary), so a new client decodes existing cache entries unchanged.Format-flag decision (reuse
SERIALIZED+ magic byte, not a fresh flag)The binary envelope keeps the existing
SERIALIZEDflag and is disambiguated from Java by the leading byte, rather than allocating a newCachedDataflag. Rationale:SERIALIZEDsemantically still means "serialized object →deserialize()"; the codec choice (Java vs binary) lives insidedeserialize(). No flag constant is reassigned or repurposed, anddecode()branch order is untouched.SERIALIZED(e.g. the admin inspector, cache-warmer) keep working without a new flag constant to propagate.SERIALIZEDthrowsStreamCorruptedException(fails loud) rather than silently decoding garbage — which a fresh low-byte flag would cause (decodeString) on old readers.EVCacheValueSerdeJavadoc):SERIALIZEDpayloads are self-describing by leading byte; a future third format must use a distinct non-colliding magic + the reserved version byte.Reserved version byte
Byte index 1 of the binary payload is reserved (always
0x00today). Reader read-and-ignores; not validated. Reason: forward-compat without an emergency reader rollout. If today's readers rejected any non-zero version, introducing a v2 in the future would require shipping reader support fleet-wide before any writer could emit a v2 byte, and a single misconfigured writer would crash all readers. By accepting any value today, future readers can branch on this byte to introduce breaking format changes backwards-compatibly.Feature Property (rollout gate)
Resolved through
EVCacheTranscoderPropertieswith a three-level fallback:<appName>.binary.serialization.enableddefault.evcache.binary.serialization.enabledfalseCompatibility
Testing
EVCacheValueSerdeTest— 17 cases via the publicEVCacheTranscoder.encode/decodeAPI:0x0C, reserved byte0x000xAC 0xED) but reads both formatsObjectOutputStream-serialized envelope still decodesEVCacheValuepassthrough (ArrayList stays on the Java path even with binary flag on)testBinaryIsSmallerThanJavaasserts the binary envelope is strictly smaller than the Java one for a representative itemtestDecodeBinaryWithOptionalAdditiveFieldIsForwardCompat— appends an unknown trailing field to a typical payload and asserts the reader still returns the typical envelope (cross-consistency check: if a real new field is added everywhere, the test continues to pass; if someone adds a non-additive field, this test fails).EVCacheTranscoderPropertiesTest— 5 cases for the resolution chain: per-app wins, global fallback when per-app unset, static default when both unset, per-app beats global, null appName routes to global../gradlew :evcache-core:testgreen. Also smoke-tested end-to-end against a locally-published snapshot in a sample Spring Boot Netflix app — round-trip put/get/delete on hashed keys passed.Chunked-payload integration is not covered by an automated test in this PR — chunking lives in
EVCacheClient.createChunks/assembleChunks, which are content-opaque (byte copy + CRC + manifest) and require a live client to exercise. The binary format introduces no new chunking risk by construction:assembleChunksreassembles bytes byte-for-byte and CRC-checks them against the manifest before handing the result to the transcoder.🤖 Generated with Claude Code